package org.ndx.majick.properties.util;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.ndx.majick.properties.Descriptable;
import org.ndx.majick.properties.Property;

/**
 * Locates and memorizes properties available in a given class.
 * This class relies mainly upon introspection to locate bean properties, 
 * expected for class that implement the FindableProperties interface.
 * @author Nicolas Delsaux
 *
 */
public class PropertiesFinder {
	/**
	 * Class internal logger. MUST NOT be used anywhere else, for logs readability
	 */
	private static final Logger logger = Logger
			.getLogger(PropertiesFinder.class.getName());
	
	private Map<Class<?>, Collection<Field>> propertiesByClass = new HashMap<Class<?>, Collection<Field>>();
	
	/**
	 * Gets properties for the input object by relying upon previous explorations, or searching in bean class.
	 * @param bean
	 * @return a collection containing all properties of the given bean.
	 */
	public Collection<Property<?>> getProperties(Object bean) {
		if(bean==null)
			return Collections.emptySet();
		if(bean instanceof Descriptable) {
			return ((Descriptable) bean).getProperties();
		} else {
			Collection<Property<?>> returned = new LinkedList<Property<?>>();
			try {
				Collection<Field> propertiesFields = getPropertiesFields(bean.getClass());
				for(Field field : propertiesFields) {
					try {
						returned.add((Property<?>) field.get(bean));
					} catch (IllegalArgumentException e) {
						if (logger.isLoggable(Level.WARNING))
							logger.log(Level.WARNING, "unable to access field "+field.getName()+" for a bean of class "+bean.getClass().getName(), e);
					} catch (IllegalAccessException e) {
						if (logger.isLoggable(Level.WARNING))
							logger.log(Level.WARNING, "unable to access field "+field.getName()+" for a bean of class "+bean.getClass().getName(), e);
					}
				}
			} catch (ForbiddenOnDescriptableException e1) {
				if (logger.isLoggable(Level.WARNING))
					logger.log(Level.WARNING, "one must be kidding, this call is for now not possible", e1);
			}
			return returned;
		}
	}

	/**
	 * Gets all properties fields from the bean class. Notice this method is not expected to work on implementors of
	 * Descriptable interface. In such a case, a ForbiddenOnDescriptableException. 
	 * @param beanClass
	 * @return
	 */
	public Collection<Field> getPropertiesFields(Class<? extends Object> beanClass) throws ForbiddenOnDescriptableException {
		if(Descriptable.class.isAssignableFrom(beanClass))
			throw new ForbiddenOnDescriptableException(beanClass);
		Collection<Field> returned = new LinkedList<Field>(); 
		if(!propertiesByClass.containsKey(beanClass)) {
			propertiesByClass.put(beanClass, locateProperties(beanClass));
		}
		returned.addAll(propertiesByClass.get(beanClass));
		if(!beanClass.equals(Object.class))
			returned.addAll(getPropertiesFields(beanClass.getSuperclass()));
		return returned;
	}

	/**
	 * Locate properties fields in input class.
	 * @param beanClass
	 * @return
	 */
	private Collection<Field> locateProperties(Class<? extends Object> beanClass) {
		Collection<Field> returned = new LinkedList<Field>();
		Field[] allFields = beanClass.getDeclaredFields();
		for(Field field : allFields) {
			if(Property.class.isAssignableFrom(field.getType())) {
				/* additionnal, but important, operation: unlock field */
				field.setAccessible(true);
				returned.add(field);
			}
		}
		return returned;
	}

	/**
	 * Build a name map from a field collection.
	 * Obviously, it will awfully fail if two fields have the same name
	 * @param fields
	 * @return
	 */
	public static Map<String, Field> getNamedFieldsMap(Collection<Field> fields) {
		Map<String, Field> returned = new TreeMap<String, Field>();
		for (Field field : fields) {
			returned.put(field.getName(), field);
		}
		return returned;
	}

	public static Map<String, Property<?>> getNamedPropertiesMap(Collection<Property<?>> properties) {
			Map<String, Property<?>> returned = new TreeMap<String, Property<?>>();
			for (Property<?> property : properties) {
				returned.put(property.getName(), property);
			}
			return returned;
	}
}
